/*-------------------------------------------------------------------------
 *
 * partdesc.c
 *		用于操作分区描述符的支持例程
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *		  src/backend/partitioning/partdesc.c
 *
 *-------------------------------------------------------------------------
 */

#include "postgres.h"

#include "access/genam.h"
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/partition.h"
#include "catalog/pg_inherits.h"
#include "partitioning/partbounds.h"
#include "partitioning/partdesc.h"
#include "storage/bufmgr.h"
#include "storage/sinval.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
#include "utils/partcache.h"
#include "utils/rel.h"
#include "utils/syscache.h"

typedef struct PartitionDirectoryData
{
	MemoryContext pdir_mcxt;
	HTAB	   *pdir_hash;
	bool		omit_detached;
}			PartitionDirectoryData;

typedef struct PartitionDirectoryEntry
{
	Oid			reloid;
	Relation	rel;
	PartitionDesc pd;
} PartitionDirectoryEntry;

static PartitionDesc fc_RelationBuildPartitionDesc(Relation fc_rel,
												bool fc_omit_detached);


/*
 * RelationGetPartitionDesc -- 获取分区描述符，如果关系是分区的
 *
 * 我们在 relcache 中保留两个 partdescs：rd_partdesc 包含所有分区
 * （甚至那些被并发标记为分离的），而 rd_partdesc_nodetach
 * 则省略（某些）分区。我们存储 pg_inherits.xmin 值用于后者，
 * 以确定在每种情况下它是否可以有效重用，因为这
 * 依赖于活动快照。
 *
 * 注意：我们安排分区描述符在 relcache 条目的引用计数变为零之前
 * 不被释放（参见 RelationClose、RelationClearRelation 和
 * RelationBuildPartitionDesc 中的黑客）。因此，即使我们返回一个
 * 指向 relcache 条目的直接指针，调用者只要（a）保持关系打开，且
 * （b）持有强锁以确保数据不会变得陈旧，就可以安全地继续使用该指针。
 */
PartitionDesc
RelationGetPartitionDesc(Relation fc_rel, bool fc_omit_detached)
{
	Assert(fc_rel->rd_rel->relkind == RELKIND_PARTITIONED_TABLE);

	/*
	 * 如果 relcache 中存在分区描述符，则使用它。然而，我们只能
	 * 在被要求包括所有分区（包括分离分区）的情况下这样做；
	 * 并且当我们知道没有分离分区时。
	 *
	 * 如果没有活动快照，分离的分区也不会被省略，
	 * 所以在这种情况下我们也可以使用缓存描述符。
	 */
	if (likely(fc_rel->rd_partdesc &&
			   (!fc_rel->rd_partdesc->detached_exist || !fc_omit_detached ||
				!ActiveSnapshotSet())))
		return fc_rel->rd_partdesc;

	/*
	 * 如果我们被要求省略分离的分区，我们可能也能够使用一个
	 * 缓存描述符。我们根据与该描述符一起保存的 pg_inherits.xmin
	 * 来判断：如果对于该活动快照未进行的 xmin
	 * 在当前活动快照中也未进行，那么我们可以使用它。
	 * 否则从头构建一个。
	 */
	if (fc_omit_detached &&
		fc_rel->rd_partdesc_nodetached &&
		ActiveSnapshotSet())
	{
		Snapshot	fc_activesnap;

		Assert(TransactionIdIsValid(fc_rel->rd_partdesc_nodetached_xmin));
		fc_activesnap = GetActiveSnapshot();

		if (!XidInMVCCSnapshot(fc_rel->rd_partdesc_nodetached_xmin, fc_activesnap))
			return fc_rel->rd_partdesc_nodetached;
	}

	return fc_RelationBuildPartitionDesc(fc_rel, fc_omit_detached);
}

/*
 * RelationBuildPartitionDesc
 *		形成关系的分区描述符，并存储在 relcache 条目中
 *
 * 分区描述符是一个复杂结构；为了避免每当 relcache 条目被刷新时
 * 释放单个元素的复杂逻辑，我们给它一个自己的内存上下文，作为
 * CacheMemoryContext 的子上下文，它可以轻松地单独删除。为了
 * 避免在此函数出现错误时在该上下文中泄漏内存，该上下文最初
 * 被创建为 CurTransactionContext 的子上下文，只有在没有进一步
 * 错误的情况下才会在最后重新归属给 CacheMemoryContext。同时，
 * 我们不会将此上下文设为当前上下文，除了在非常简短的代码段中，
 * 以避免我们的被调用者在其自身中分配内存而导致的永久性泄漏。
 *
 * 作为特例，请求省略正在分离的分区（并且包含此类分区）的分区
 * 描述符是临时的，并且与 relcache 条目无关。这些描述符仅在
 * 请求的 Portal 期间存在，因此我们使用对应的内存上下文
 * 来处理它们。
 */
static PartitionDesc
fc_RelationBuildPartitionDesc(Relation fc_rel, bool fc_omit_detached)
{
	PartitionDesc fc_partdesc;
	PartitionBoundInfo fc_boundinfo = NULL;
	List	   *fc_inhoids;
	PartitionBoundSpec **fc_boundspecs = NULL;
	Oid		   *fc_oids = NULL;
	bool	   *fc_is_leaf = NULL;
	bool		fc_detached_exist;
	bool		fc_is_omit;
	TransactionId fc_detached_xmin;
	ListCell   *fc_cell;
	int			fc_i,
				fc_nparts;
	bool		fc_retried = false;
	PartitionKey fc_key = RelationGetPartitionKey(fc_rel);
	MemoryContext fc_new_pdcxt;
	MemoryContext fc_oldcxt;
	int		   *fc_mapping;

retry:

	/*
	 * 从 pg_inherits 获取分区 OID。这使用单个快照来
	 * 获取子表的列表，因此尽管可能有更多的子表正在并发添加
	 * 或删除，但此函数返回的内容在某个明确定义的时间点是
	 * 准确的。
	 */
	fc_detached_exist = false;
	fc_detached_xmin = InvalidTransactionId;
	fc_inhoids = find_inheritance_children_extended(RelationGetRelid(fc_rel),
												 fc_omit_detached, NoLock,
												 &fc_detached_exist,
												 &fc_detached_xmin);

	fc_nparts = list_length(fc_inhoids);

	/* 为 OID、叶子标志和边界规格分配工作数组。 */
	if (fc_nparts > 0)
	{
		fc_oids = (Oid *) palloc(fc_nparts * sizeof(Oid));
		fc_is_leaf = (bool *) palloc(fc_nparts * sizeof(bool));
		fc_boundspecs = palloc(fc_nparts * sizeof(PartitionBoundSpec *));
	}

	/* 收集每个分区的边界规格节点。 */
	fc_i = 0;
	foreach(fc_cell, fc_inhoids)
	{
		Oid			fc_inhrelid = lfirst_oid(fc_cell);
		HeapTuple	fc_tuple;
		PartitionBoundSpec *fc_boundspec = NULL;

		/* 尝试从 catcache 中获取元组，以提高速度。 */
		fc_tuple = SearchSysCache1(RELOID, fc_inhrelid);
		if (HeapTupleIsValid(fc_tuple))
		{
			Datum		fc_datum;
			bool		fc_isnull;

			fc_datum = SysCacheGetAttr(RELOID, fc_tuple,
									Anum_pg_class_relpartbound,
									&fc_isnull);
			if (!fc_isnull)
				fc_boundspec = stringToNode(TextDatumGetCString(fc_datum));
			ReleaseSysCache(fc_tuple);
		}

		/*
		 * 这里可能出现两个问题。首先，可能有一个并发的 ATTACH
		 * PARTITION 正在添加一个新分区，但
		 * 系统缓存中没有它，或者它的副本尚未设置
		 * relpartbound。我们不能仅仅调用 AcceptInvalidationMessages()，
		 * 因为其他进程可能已经将自己从
		 * ProcArray 中移除，但尚未将其无效消息添加到
		 * 共享队列中。我们通过直接读取 pg_class
		 * 来解决此问题，以获取所需的元组。
		 *
		 * 如果最近分离的分区也被删除，那么我们从扫描中不会
		 * 获取任何元组。在这种情况下，我们也会重试，下次通过
		 * 这里时，我们将看不到该分区。
		 *
		 * 另一个问题是，DETACH CONCURRENTLY 正在移除一个分区，
		 * 这分两步进行：首先将其标记为“分离待处理”，提交，
		 * 然后取消设置 relpartbound。如果
		 * find_inheritance_children_extended 包含该分区，但我们
		 * 在下面看到 DETACH CONCURRENTLY 已重置
		 * relpartbound，我们会看到一个不一致的视图。
		 * （不一致是因为下面的 table_open 读取了无效消息。）
		 * 我们通过重试 find_inheritance_children_extended() 来保护
		 * 这一点。
		 */
		if (fc_boundspec == NULL)
		{
			Relation	fc_pg_class;
			SysScanDesc fc_scan;
			ScanKeyData fc_key[1];

			fc_pg_class = table_open(RelationRelationId, AccessShareLock);
			ScanKeyInit(&fc_key[0],
						Anum_pg_class_oid,
						BTEqualStrategyNumber, F_OIDEQ,
						ObjectIdGetDatum(fc_inhrelid));
			fc_scan = systable_beginscan(fc_pg_class, ClassOidIndexId, true,
									  NULL, 1, fc_key);

			/*
			 * 我们可能从扫描中获得一个元组（正常情况），如果表
			 * 与此同时已被删除，则获得零个元组。
			 */
			fc_tuple = systable_getnext(fc_scan);
			if (HeapTupleIsValid(fc_tuple))
			{
				Datum		fc_datum;
				bool		fc_isnull;

				fc_datum = heap_getattr(fc_tuple, Anum_pg_class_relpartbound,
									 RelationGetDescr(fc_pg_class), &fc_isnull);
				if (!fc_isnull)
					fc_boundspec = stringToNode(TextDatumGetCString(fc_datum));
			}
			systable_endscan(fc_scan);
			table_close(fc_pg_class, AccessShareLock);

			/*
			 * 如果我们仍然没有获得 relpartbound 值（无论是因为
			 * boundspec 为 null 还是因为没有元组），那么这必然
			 * 是由于 DETACH CONCURRENTLY。按照上面的解释从头
			 * 重新开始。我们仅此进行一次，原因有两个：首先，
			 * 只有一个 DETACH CONCURRENTLY 会话可以同时影响我们，
			 * 因为它们都必须等待在运行时的快照；第二，为了避免在
			 * 目录损坏的情况下可能的无限循环。
			 *
			 * 请注意，当前内存上下文的生命周期足够短，因此
			 * 我们无须担心内存泄露。
			 */
			if (!fc_boundspec && !fc_retried)
			{
				AcceptInvalidationMessages();
				fc_retried = true;
				goto retry;
			}
		}

		/* 安全检查。 */
		if (!fc_boundspec)
			elog(ERROR, "missing relpartbound for relation %u", fc_inhrelid);
		if (!IsA(fc_boundspec, PartitionBoundSpec))
			elog(ERROR, "invalid relpartbound for relation %u", fc_inhrelid);

		/*
		 * 如果 PartitionBoundSpec 表示这是默认分区，则其
		 * OID 应与 pg_partitioned_table.partdefid 匹配；如果不匹配，
		 * 则目录已损坏。
		 */
		if (fc_boundspec->is_default)
		{
			Oid			fc_partdefid;

			fc_partdefid = get_default_partition_oid(RelationGetRelid(fc_rel));
			if (fc_partdefid != fc_inhrelid)
				elog(ERROR, "expected partdefid %u, but got %u",
					 fc_inhrelid, fc_partdefid);
		}

		/* 保存结果。 */
		fc_oids[fc_i] = fc_inhrelid;
		fc_is_leaf[fc_i] = (get_rel_relkind(fc_inhrelid) != RELKIND_PARTITIONED_TABLE);
		fc_boundspecs[fc_i] = fc_boundspec;
		++fc_i;
	}

	/*
	 * 创建 PartitionBoundInfo 和映射，在调用者的上下文中工作。
	 * 这可能会失败，但即使这样我们也没有造成任何损害。
	 */
	if (fc_nparts > 0)
		fc_boundinfo = partition_bounds_create(fc_boundspecs, fc_nparts, fc_key, &fc_mapping);

	/*
	 * 现在构建实际的 relcache 分区描述符，将所有数据复制到一个新的、小的上下文中。根据上述评论，我们在完成之前不会将其作为长期上下文。
	 */
	fc_new_pdcxt = AllocSetContextCreate(CurTransactionContext,
									  "partition descriptor",
									  ALLOCSET_SMALL_SIZES);
	MemoryContextCopyAndSetIdentifier(fc_new_pdcxt,
									  RelationGetRelationName(fc_rel));

	fc_partdesc = (PartitionDescData *)
		MemoryContextAllocZero(fc_new_pdcxt, sizeof(PartitionDescData));
	fc_partdesc->nparts = fc_nparts;
	fc_partdesc->detached_exist = fc_detached_exist;
	/* 如果没有分区，其余的 partdesc 可以保持为零 */
	if (fc_nparts > 0)
	{
		fc_oldcxt = MemoryContextSwitchTo(fc_new_pdcxt);
		fc_partdesc->boundinfo = partition_bounds_copy(fc_boundinfo, fc_key);
		fc_partdesc->oids = (Oid *) palloc(fc_nparts * sizeof(Oid));
		fc_partdesc->is_leaf = (bool *) palloc(fc_nparts * sizeof(bool));

		/*
		 * 将原始数组中的 OID 分配到结果数组的映射索引中。前者中 OID 的顺序由检索它们的目录扫描定义，而后者中则由分区边界的标准化表示定义。还保存每个分区的叶子状态。
		 */
		for (fc_i = 0; fc_i < fc_nparts; fc_i++)
		{
			int			fc_index = fc_mapping[fc_i];

			fc_partdesc->oids[fc_index] = fc_oids[fc_i];
			fc_partdesc->is_leaf[fc_index] = fc_is_leaf[fc_i];
		}
		MemoryContextSwitchTo(fc_oldcxt);
	}

	/*
	 * 我们是在处理省略了分离分区的 partdesc，还是包括它的那个？
	 *
	 * 请注意，如果通过目录的扫描发现某个分区已被分离，但声明这一点的 pg_inherit 元组在活动快照下不可见（在这种情况下，find_inheritance_children_extended 不会设置 detached_xmin），我们认为没有“可省略”的分离分区。
	 */
	fc_is_omit = fc_omit_detached && fc_detached_exist && ActiveSnapshotSet() &&
		TransactionIdIsValid(fc_detached_xmin);

	/*
	 * 我们有一个完全有效的 partdesc。重新父化它，以便它具有正确的生命周期。
	 */
	MemoryContextSetParent(fc_new_pdcxt, CacheMemoryContext);

	/*
	 * 将其存储到 relcache 中。
	 *
	 * 但首先，一个权宜之计：如果有一个旧的上下文用于该类型的描述符，它包含一个旧的分区描述符，可能仍然在某处被引用。通过将其重新附加为新上下文的子上下文来保留它，同时不泄漏它。最终它将被 RelationClose 或 RelationClearRelation 丢弃。（我们在 rd_pdcxt 中保留常规的 partdesc，在 rd_pddcxt 中保留排除分离分区的 partdesc。）
	 */
	if (fc_is_omit)
	{
		if (fc_rel->rd_pddcxt != NULL)
			MemoryContextSetParent(fc_rel->rd_pddcxt, fc_new_pdcxt);
		fc_rel->rd_pddcxt = fc_new_pdcxt;
		fc_rel->rd_partdesc_nodetached = fc_partdesc;

		/*
		 * 对于构建时排除了分离分区的 partdesc，我们将其单独保存，并记录被省略的分离分区的 pg_inherits.xmin；这告知未来潜在用户在使用此缓存的 partdesc 时，仅在交叉检查 xmin 确实可见于将要使用的快照之后才能使用它。
		 */
		Assert(TransactionIdIsValid(fc_detached_xmin));
		fc_rel->rd_partdesc_nodetached_xmin = fc_detached_xmin;
	}
	else
	{
		if (fc_rel->rd_pdcxt != NULL)
			MemoryContextSetParent(fc_rel->rd_pdcxt, fc_new_pdcxt);
		fc_rel->rd_pdcxt = fc_new_pdcxt;
		fc_rel->rd_partdesc = fc_partdesc;
	}

	return fc_partdesc;
}

/*
 * CreatePartitionDirectory
 *		创建一个新的分区目录对象。
 */
PartitionDirectory
CreatePartitionDirectory(MemoryContext fc_mcxt, bool fc_omit_detached)
{
	MemoryContext fc_oldcontext = MemoryContextSwitchTo(fc_mcxt);
	PartitionDirectory fc_pdir;
	HASHCTL		fc_ctl;

	fc_pdir = palloc(sizeof(PartitionDirectoryData));
	fc_pdir->pdir_mcxt = fc_mcxt;

	fc_ctl.keysize = sizeof(Oid);
	fc_ctl.entrysize = sizeof(PartitionDirectoryEntry);
	fc_ctl.hcxt = fc_mcxt;

	fc_pdir->pdir_hash = hash_create("partition directory", 256, &fc_ctl,
								  HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
	fc_pdir->omit_detached = fc_omit_detached;

	MemoryContextSwitchTo(fc_oldcontext);
	return fc_pdir;
}

/*
 * PartitionDirectoryLookup
 *		在目录中查找关系的分区描述符。
 *
 * 此函数的目的是确保每次查找时我们都能为每个关系获得相同的 PartitionDesc。在并发 DDL 的情况下，可能会构造出不同的 PartitionDesc，其目录状态的视图不同，但只要使用相同的 PartitionDirectory，任何特定的 OID 始终会获得相同的 PartitionDesc。
 */
PartitionDesc
PartitionDirectoryLookup(PartitionDirectory fc_pdir, Relation fc_rel)
{
	PartitionDirectoryEntry *fc_pde;
	Oid			fc_relid = RelationGetRelid(fc_rel);
	bool		fc_found;

	fc_pde = hash_search(fc_pdir->pdir_hash, &fc_relid, HASH_ENTER, &fc_found);
	if (!fc_found)
	{
		/*
		 * 我们必须对关系保持引用计数，以便我们指向的 PartitionDesc 不会被销毁。
		 */
		RelationIncrementReferenceCount(fc_rel);
		fc_pde->rel = fc_rel;
		fc_pde->pd = RelationGetPartitionDesc(fc_rel, fc_pdir->omit_detached);
		Assert(fc_pde->pd != NULL);
	}
	return fc_pde->pd;
}

/*
 * DestroyPartitionDirectory
 *		销毁一个分区目录。
 *
 * 释放我们持有的引用计数。
 */
void DestroyPartitionDirectory(PartitionDirectory fc_pdir)
{
	HASH_SEQ_STATUS fc_status;
	PartitionDirectoryEntry *fc_pde;

	hash_seq_init(&fc_status, fc_pdir->pdir_hash);
	while ((fc_pde = hash_seq_search(&fc_status)) != NULL)
		RelationDecrementReferenceCount(fc_pde->rel);
}

/*
 * get_default_oid_from_partdesc
 *
 * 给定一个分区描述符，返回默认分区的 OID，如果
 * 存在的话；否则，返回 InvalidOid。
 */
Oid get_default_oid_from_partdesc(PartitionDesc fc_partdesc)
{
	if (fc_partdesc && fc_partdesc->boundinfo &&
		partition_bound_has_default(fc_partdesc->boundinfo))
		return fc_partdesc->oids[fc_partdesc->boundinfo->default_index];

	return InvalidOid;
}
